Importazione dati e creazione grafo

# Lettura e pulizia dati collaborazioni
df <- read.csv("Collaborazioni_finale_oggi.csv", stringsAsFactors = FALSE)
head(df)
##     main_artist featured_artist         track_name release_year
## 1 Sfera Ebbasta           Shiva             SNTMNG         2025
## 2 Sfera Ebbasta           Shiva NON METTERCI BECCO         2025
## 3 Sfera Ebbasta           Shiva          SEI PERSA         2025
## 4 Sfera Ebbasta           Shiva    MOLECOLE SPRITE         2025
## 5 Sfera Ebbasta           Shiva            MAYBACH         2025
## 6 Sfera Ebbasta           Shiva               NEON         2025
df_clean <- df %>%
  filter(main_artist != featured_artist) %>%
  distinct(main_artist, featured_artist, track_name, release_year, .keep_all = TRUE)

cat("Numero righe dopo pulizia:", nrow(df_clean), "\n")
## Numero righe dopo pulizia: 3108
write.csv(df_clean, "Collaborazioni_finale_oggi_clean.csv", row.names = FALSE)

# Lettura artisti e preparazione grafo
artist <- read.csv("artisti.csv") %>% select(-id)
feat <- read_csv("Collaborazioni_finale_oggi_clean.csv")
## Rows: 3108 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): main_artist, featured_artist, track_name
## dbl (1): release_year
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
colnames(feat)[1:2] <- c("from", "to")

# Normalizzazione nomi
artist$artist <- str_trim(tolower(artist$artist))
feat$from <- str_trim(tolower(feat$from))
feat$to <- str_trim(tolower(feat$to))

# Filtro collaborazioni valide e creazione grafo
feat_clean <- feat %>%
  filter(from %in% artist$artist & to %in% artist$artist)

g <- graph_from_data_frame(feat_clean, directed = TRUE, vertices = artist)
E(g)$release_year <- feat_clean$release_year

cat("Numero nodi:", length(V(g)), "\n")
## Numero nodi: 200
cat("Numero archi:", length(E(g)), "\n")
## Numero archi: 3108
cat("Densità:", round(edge_density(g), 4), "\n")
## Densità: 0.0781
vertex_attr_names(g)
## [1] "name"   "genres"
edge_attr_names(g)
## [1] "track_name"   "release_year"

Calcolo centralità e PageRank

# Calcolo di tutte le centralità
centralities <- data.frame(
  node = V(g)$name,
  degree = degree(g),
  in_degree = degree(g, mode = "in"),
  out_degree = degree(g, mode = "out"),
  pagerank = page_rank(g)$vector,
  betweenness = betweenness(g),
  closeness = closeness(g)
)

# Funzione per creare grafici
create_centrality_plot <- function(data, metric, title, ylabel, color) {
  top_data <- data %>%
    arrange(desc(.data[[metric]])) %>%
    slice_head(n = 10)
  
  ggplot(top_data, aes(x = reorder(node, .data[[metric]]), y = .data[[metric]])) +
    geom_col(fill = color) +
    coord_flip() +
    labs(title = title, x = "Artista", y = ylabel) +
    theme_minimal()
}

# Grafici centralità
create_centrality_plot(centralities, "degree", "Degree totale", 
                      "Numero totale di collaborazioni", "#1cd463")

create_centrality_plot(centralities, "in_degree", "In-degree", 
                      "Numero di featuring eseguiti come ft-artist", "#3d811c")

create_centrality_plot(centralities, "out_degree", "Out-degree", 
                      "Numero di featuring eseguiti come main-artist", "#27ae60")

# PageRank wordcloud
top_pagerank_wc <- centralities %>%
  arrange(desc(pagerank)) %>%
  slice_head(n = 35)

wordcloud(
  words = top_pagerank_wc$node,
  freq = top_pagerank_wc$pagerank,
  scale = c(4, 0.8),
  random.order = FALSE,
  rot.per = 0.1,
  colors = brewer.pal(8, "Greens")
)

# Top_nodi
visualize_top_nodes <- function(g, centrality_data, metric, title, top_n = 3) {
  g_undirected <- as_undirected(g, mode = "collapse")
  
  top_nodes <- centrality_data %>%
    arrange(desc(.data[[metric]])) %>%
    slice_head(n = top_n)
  

  V(g_undirected)$color <- "lightgrey"
  V(g_undirected)[name %in% top_nodes$node]$color <- "#1cd463"
  E(g_undirected)$color <- "#676b65"
  
  # Archi incidenti
  for (node in top_nodes$node) {
    incident_edges <- incident(g_undirected, V(g_undirected)[name == node], mode = "all")
    E(g_undirected)[incident_edges]$color <- "#1cd463"
  }
  
  layout_fr <- layout_with_fr(g_undirected)
  nodes_df <- data.frame(
    name = V(g_undirected)$name,
    color = V(g_undirected)$color,
    x = layout_fr[,1],
    y = layout_fr[,2]
  ) %>%
    left_join(top_nodes %>% select(node, all_of(metric)), by = c("name" = "node")) %>%
    mutate(
      is_top = !is.na(.data[[metric]]),
      size = ifelse(is_top, scales::rescale(.data[[metric]], to = c(7, 15)), 5)
    )
  
  ggraph(g_undirected, layout = "manual", x = nodes_df$x, y = nodes_df$y) +
    geom_edge_link(aes(color = I(color)), alpha = 0.6) +
    geom_node_point(data = filter(nodes_df, !is_top),
                    aes(x = x, y = y, fill = I(color)),
                    shape = 21, size = 5, stroke = 0.8, colour = "black", alpha = 0.3) +
    geom_node_point(data = filter(nodes_df, is_top),
                    aes(x = x, y = y, fill = I(color), size = size),
                    shape = 21, stroke = 1, colour = "black") +
    geom_node_text(
      data = filter(nodes_df, is_top),
      aes(x = x, y = y, label = name),
      repel = TRUE, size = 5, fontface = "bold", color = "black"
    ) +
    labs(title = title) +
    theme_void() +
    guides(size = "none")
}

# Visualizzazioni betweenness e closeness
visualize_top_nodes(g, centralities, "betweenness", "Top 3 nodi per Betweenness")

visualize_top_nodes(g, centralities, "closeness", "Top 3 nodi per Closeness")

Confronto centralità: degree, betweenness, closeness, PageRank

# Normalizzazione e confronto centralità
centralities_norm <- centralities %>%
  mutate(
    betweenness = betweenness(g, normalized = TRUE),
    closeness = closeness(g, normalized = TRUE)
  ) %>%
  select(artist = node, degree, betweenness, closeness, pagerank)

centralities_melt <- melt(centralities_norm, id.vars = "artist")
green_palette <- c("#1b4d3e", "#3d811c", "#1cd463", "#d9f0a3")

ggplot(centralities_melt, aes(x = value, fill = variable)) +
  geom_histogram(bins = 50, alpha = 0.7, position = "identity") +
  facet_wrap(~variable, scales = "free_x") +
  scale_fill_manual(values = green_palette) +
  labs(x = "Valore centralità", y = "Frequenza") +
  theme_minimal()

Funzione per analisi temporali

# Funzione generale per analisi per anno
analyze_by_year <- function(g, years = 2009:2024, metric_func) {
  results <- data.frame()
  
  for (yr in years) {
    edges_year <- E(g)[E(g)$release_year == yr]
    if (length(edges_year) > 0) {
      g_year <- subgraph.edges(g, edges_year, delete.vertices = TRUE)
      metric_val <- metric_func(g_year)
    } else {
      metric_val <- NA
    }
    results <- rbind(results, data.frame(anno = yr, valore = metric_val))
  }
  return(results)
}

# Funzione per grafici temporali
create_temporal_plot <- function(data, title, ylabel, as_percentage = FALSE) {
  if (as_percentage) {
    data <- data %>%
      mutate(
        valore_pct = valore * 100,
        label = ifelse(!is.na(valore), paste0(round(valore * 100, 1), "%"), "")
      )
    p <- ggplot(data, aes(x = anno, y = valore_pct))
  } else {
    p <- ggplot(data, aes(x = anno, y = valore))
  }
  
  p + geom_area(fill = "#1cd463", alpha = 0.6) +
    geom_line(color = "#3d811c", size = 1.2) +
    geom_point(color = "#00441b", size = 3) +
    labs(title = title, x = "Anno", y = ylabel) +
    {if (as_percentage) scale_y_continuous(labels = function(x) paste0(x, "%"))} +
    theme_minimal(base_size = 15)
}

Analisi di reciprocità del grafo

recip_data <- analyze_by_year(g, 2009:2024, function(g_year) {
  if (ecount(g_year) > 0) reciprocity(g_year) else NA
})
## Warning: `subgraph.edges()` was deprecated in igraph 2.1.0.
## ℹ Please use `subgraph_from_edges()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
create_temporal_plot(recip_data, "Evoluzione della Reciprocità", "Reciprocità (%)", TRUE)
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

Transitività

trans_data <- analyze_by_year(g, 2009:2024, function(g_year) {
  g_und <- as.undirected(g_year, mode = "collapse")
  tryCatch(transitivity(g_und, type = "global"), error = function(e) NA)
})
## Warning: `as.undirected()` was deprecated in igraph 2.1.0.
## ℹ Please use `as_undirected()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
create_temporal_plot(trans_data, "Evoluzione della Transitività", "Transitività (%)", TRUE)

Paradosso dell’amico

deg <- degree(g, mode = "all")
neighbor_deg_mean <- knn(g, mode = "all")$knn

paradox_df <- data.frame(
  artist = V(g)$name,
  degree = deg,
  neighbor_degree_mean = neighbor_deg_mean,
  diff = neighbor_deg_mean - deg
)

paradox_value <- mean(paradox_df$neighbor_degree_mean > paradox_df$degree, na.rm = TRUE)

paradox_data <- data.frame(
  condizione = c("Vale il paradosso", "Non vale il paradosso"),
  valore = c(paradox_value, 1 - paradox_value)
) %>%
  mutate(
    percentuale = paste0(round(valore * 100, 1), "%"),
    ypos = cumsum(valore) - 0.5 * valore
  )

ggplot(paradox_data, aes(x = "", y = valore, fill = condizione)) +
  geom_bar(stat = "identity", width = 1, color = "white") +
  coord_polar("y") +
  geom_text(aes(y = ypos, label = percentuale), color = "white", size = 5) +
  scale_fill_manual(values = c("Vale il paradosso" = "#1cd463", 
                               "Non vale il paradosso" = "#3d811c")) +
  labs(title = "Paradosso dell'amico tra gli artisti") +
  theme_void() +
  theme(legend.title = element_blank())

Analisi di assortatività per grado

# Assortatività globale per grado
g_sub <- subgraph_from_edges(g, E(g)[release_year >= 2009 & release_year <= 2024], 
                            delete.vertices = TRUE)
assort_degree <- assortativity_degree(g_sub)
cat("Assortatività per grado:", round(assort_degree, 3), "\n")
## Assortatività per grado: 0.364
# Assortatività per anno
assort_data <- analyze_by_year(g, 2009:2024, function(g_year) {
  if (ecount(g_year) > 0 && vcount(g_year) > 1) {
    assortativity_degree(g_year)
  } else {
    NA
  }
})

ggplot(assort_data, aes(x = anno, y = valore)) +
  geom_line(color = "#1cd463", linewidth = 1.2) +
  geom_point(color = "#1cd463", size = 2) +
  geom_smooth(method = "loess", se = FALSE, color = "gray40", linetype = "dashed") +
  labs(title = "Evoluzione dell'assortatività per grado",
       x = "Anno", y = "Indice di assortatività") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

Collaborazioni intra-genere nel tempo

# Analisi collaborazioni intra vs inter genere
edges_df <- igraph::as_data_frame(g, what = "edges") %>%
  mutate(
    from_genre = V(g)$genres[match(from, V(g)$name)],
    to_genre = V(g)$genres[match(to, V(g)$name)],
    same_genre = from_genre == to_genre,
    year = E(g)$release_year
  )

intra_genre_by_year <- edges_df %>%
  filter(year >= 2009 & year <= 2024) %>%
  group_by(year) %>%
  summarise(
    intra = sum(same_genre, na.rm = TRUE),
    inter = sum(!same_genre, na.rm = TRUE)
  ) %>%
  pivot_longer(cols = c("intra", "inter"), names_to = "type", values_to = "count")

ggplot(intra_genre_by_year, aes(x = year, y = count, color = type)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  theme_minimal() +
  scale_x_continuous(breaks = scales::pretty_breaks(n = 10)) +
  scale_color_manual(values = c("intra" = "#1cd463", "inter" = "#3d811c")) +
  labs(title = "Collaborazioni intra- vs inter- genere",
       x = "Anno", y = "Numero di collaborazioni", color = "Tipo di ft")

Analisi temporale: numero di collaborazioni per anno

year_table <- as.data.frame(table(E(g)$release_year)) %>%
  setNames(c("year", "collaborations")) %>%
  mutate(year = as.numeric(as.character(year))) %>%
  filter(year >= 2009 & year <= 2024) %>%
  arrange(year)

ggplot(year_table, aes(x = year, y = collaborations)) +
  geom_line(color = "#1cd463", linewidth = 1.2) +
  geom_point(color = "#1cd463", size = 2) +
  theme_minimal() +
  scale_x_continuous(breaks = scales::pretty_breaks(n = 10)) +
  labs(title = "Evoluzione dei ft nel tempo",
       x = "Anno", y = "Numero di collaborazioni")

Visualizzazione crescita del grafo

df_growth <- read_csv("collaborazioni_finale_oggi_clean.csv") %>%
  filter(release_year >= 2009) %>%
  distinct(main_artist, featured_artist, release_year)
## Rows: 3108 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): main_artist, featured_artist, track_name
## dbl (1): release_year
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
years <- sort(unique(df_growth$release_year))
g_full <- graph_from_data_frame(df_growth %>% select(main_artist, featured_artist), 
                                directed = TRUE)

# Layout fisso per l'animazione
lay <- layout_with_kk(g_full)
lay_df <- as.data.frame(lay) %>%
  setNames(c("x", "y")) %>%
  mutate(node = V(g_full)$name)

# Preparazione dati per animazione
nodes_list <- list()
edges_list <- list()

for (year in years) {
  temp_edges <- df_growth %>% 
    filter(release_year <= year) %>% 
    select(main_artist, featured_artist)
  
  g_year <- graph_from_data_frame(temp_edges, directed = TRUE, 
                                  vertices = V(g_full)$name)
  active_nodes <- V(g_year)$name
  
  nodes_list[[as.character(year)]] <- lay_df %>%
    filter(node %in% active_nodes) %>%
    mutate(year = year)
  
  edges_df <- igraph::as_data_frame(g_year, what = "edges") %>%
    left_join(lay_df, by = c("from" = "node")) %>%
    left_join(lay_df, by = c("to" = "node"), suffix = c("", "_end")) %>%
    mutate(year = year)
  
  edges_list[[as.character(year)]] <- edges_df
}

nodes_all <- bind_rows(nodes_list)
edges_all <- bind_rows(edges_list)

p <- ggplot() +
  geom_segment(data = edges_all,
               aes(x = x, y = y, xend = x_end, yend = y_end),
               alpha = 0.3, color = "#676b65") +
  geom_point(data = nodes_all, aes(x = x, y = y),
             size = 2, color = "#1cd463") +
  theme_void() +
  ggtitle("Anno: {closest_state}") +
  transition_states(year, transition_length = 2, state_length = 1) +
  ease_aes("cubic-in-out")
animate(p, nframes = length(years) * 3, fps = 10, width = 800, height = 600,
        renderer = gifski_renderer("network_evolution.gif"))

Girvan-Newman Community Detection

g_undirected <- as_undirected(g, mode = "collapse")
girvan_comm <- cluster_edge_betweenness(g_undirected)

comm_sizes <- sizes(girvan_comm)
sorted_indices <- order(comm_sizes, decreasing = TRUE)
top_n <- 3
top_comms <- sorted_indices[1:top_n]

cat("Girvan-Newman - numero totale comunità:", length(comm_sizes), "\n\n")
## Girvan-Newman - numero totale comunità: 61
for (i in seq_along(sorted_indices)) {
  cat(sprintf("Comunità %d: %d nodi\n", sorted_indices[i], comm_sizes[sorted_indices[i]]))
}
## Comunità 1: 92 nodi
## Comunità 3: 23 nodi
## Comunità 5: 21 nodi
## Comunità 2: 4 nodi
## Comunità 6: 2 nodi
## Comunità 8: 2 nodi
## Comunità 12: 2 nodi
## Comunità 4: 1 nodi
## Comunità 7: 1 nodi
## Comunità 9: 1 nodi
## Comunità 10: 1 nodi
## Comunità 11: 1 nodi
## Comunità 13: 1 nodi
## Comunità 14: 1 nodi
## Comunità 15: 1 nodi
## Comunità 16: 1 nodi
## Comunità 17: 1 nodi
## Comunità 18: 1 nodi
## Comunità 19: 1 nodi
## Comunità 20: 1 nodi
## Comunità 21: 1 nodi
## Comunità 22: 1 nodi
## Comunità 23: 1 nodi
## Comunità 24: 1 nodi
## Comunità 25: 1 nodi
## Comunità 26: 1 nodi
## Comunità 27: 1 nodi
## Comunità 28: 1 nodi
## Comunità 29: 1 nodi
## Comunità 30: 1 nodi
## Comunità 31: 1 nodi
## Comunità 32: 1 nodi
## Comunità 33: 1 nodi
## Comunità 34: 1 nodi
## Comunità 35: 1 nodi
## Comunità 36: 1 nodi
## Comunità 37: 1 nodi
## Comunità 38: 1 nodi
## Comunità 39: 1 nodi
## Comunità 40: 1 nodi
## Comunità 41: 1 nodi
## Comunità 42: 1 nodi
## Comunità 43: 1 nodi
## Comunità 44: 1 nodi
## Comunità 45: 1 nodi
## Comunità 46: 1 nodi
## Comunità 47: 1 nodi
## Comunità 48: 1 nodi
## Comunità 49: 1 nodi
## Comunità 50: 1 nodi
## Comunità 51: 1 nodi
## Comunità 52: 1 nodi
## Comunità 53: 1 nodi
## Comunità 54: 1 nodi
## Comunità 55: 1 nodi
## Comunità 56: 1 nodi
## Comunità 57: 1 nodi
## Comunità 58: 1 nodi
## Comunità 59: 1 nodi
## Comunità 60: 1 nodi
## Comunità 61: 1 nodi
# Colorazione comunità
green_palette <- c("#d9f0a3", "#78c679", "#238443")
grey_color <- "#B0B0B0"
community_colors <- rep(grey_color, length(comm_sizes))
community_colors[top_comms] <- green_palette

V(g_undirected)$color <- community_colors[membership(girvan_comm)]

ggraph(g_undirected, layout = "fr") +
  geom_edge_link(alpha = 0.4, color = "#676b65") +
  geom_node_point(aes(fill = I(color)), shape = 21, size = 4, 
                  stroke = 0.8, colour = "black") +
  theme_void() +
  labs(title = "Community detection")

# Analisi comunità con singolo nodo
solo_node_comms <- which(comm_sizes == 1)
cat("\nNumero di comunità con un solo nodo:", length(solo_node_comms), "\n")
## 
## Numero di comunità con un solo nodo: 54
if (length(solo_node_comms) > 0) {
  cat("Artisti che fanno parte di una comunità con un solo nodo:\n")
  for (i in solo_node_comms) {
    node_id <- which(membership(girvan_comm) == i)
    artist_name <- V(g_undirected)$name[node_id]
    cat(sprintf("- Comunità %d: %s\n", i, artist_name))
  }
}
## Artisti che fanno parte di una comunità con un solo nodo:
## - Comunità 4: ultimo
## - Comunità 7: vasco rossi
## - Comunità 9: linkin park
## - Comunità 10: billie eilish
## - Comunità 11: imagine dragons
## - Comunità 13: lucio battisti
## - Comunità 14: higashi
## - Comunità 15: lucio corsi
## - Comunità 16: pino daniele
## - Comunità 17: ligabue
## - Comunità 18: queen
## - Comunità 19: noemi
## - Comunità 20: 3d
## - Comunità 21: coma_cose
## - Comunità 22: twenty one pilots
## - Comunità 23: max pezzali
## - Comunità 24: negramaro
## - Comunità 25: zucchero
## - Comunità 26: alex warren
## - Comunità 27: le-one
## - Comunità 28: brunori sas
## - Comunità 29: benson boone
## - Comunità 30: adele
## - Comunità 31: katy perry
## - Comunità 32: fabrizio de andré
## - Comunità 33: maroon 5
## - Comunità 34: sick luke
## - Comunità 35: emma
## - Comunità 36: sza
## - Comunità 37: hugel
## - Comunità 38: myke towers
## - Comunità 39: pink floyd
## - Comunità 40: modà
## - Comunità 41: arctic monkeys
## - Comunità 42: justin bieber
## - Comunità 43: mahmood
## - Comunità 44: alessandra amoroso
## - Comunità 45: rhove
## - Comunità 46: nitro
## - Comunità 47: ava
## - Comunità 48: alleh
## - Comunità 49: yorghaki
## - Comunità 50: dark polo gang
## - Comunità 51: 21 savage
## - Comunità 52: bobo
## - Comunità 53: antonello venditti
## - Comunità 54: måneskin
## - Comunità 55: red hot chili peppers
## - Comunità 56: tate mcrae
## - Comunità 57: sabrina carpenter
## - Comunità 58: thegiornalisti
## - Comunità 59: green day
## - Comunità 60: francesco gabbani
## - Comunità 61: liberato

Heatmap generi vs comunità

membership_girvan <- membership(girvan_comm)

# Analisi distribuzione generi per comunità
df_gen_comm <- data.frame(
  artist = names(membership_girvan),
  community = as.factor(membership_girvan),
  genre = V(g_undirected)$genres[match(names(membership_girvan), V(g_undirected)$name)]
) %>%
  filter(!is.na(genre))

# Top 3 comunità per dimensione
top_3_communities <- df_gen_comm %>%
  count(community) %>%
  arrange(desc(n)) %>%
  slice_head(n = 3) %>%
  pull(community)

genre_community_prop <- df_gen_comm %>%
  filter(community %in% top_3_communities) %>%
  count(genre, community) %>%
  group_by(community) %>%
  mutate(proportion = n / sum(n)) %>%
  ungroup() %>%
  select(genre, community, proportion) %>%
  pivot_wider(names_from = community, values_from = proportion, values_fill = 0) %>%
  pivot_longer(-genre, names_to = "Community", values_to = "Proportion") %>%
  rename(Genre = genre)

ggplot(genre_community_prop, aes(x = Community, y = Genre, fill = Proportion)) +
  geom_tile(color = "white") +
  scale_fill_gradient(low = "white", high = "#1cd463") +
  labs(title = "Distribuzione dei generi per comunità",
       x = "Comunità", y = "Genere", fill = "Proporzione") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        panel.grid = element_blank())